package pl.edu.pb.wi.pwnography.modules;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

public class KNNTwoAttributes extends KNearestNeighbor {
    private static final Logger log = Logger.getLogger(KNNTwoAttributes.class
	    .getName());

    public static List<Object> leaveOneOut(List<Object> firstColumnValues,
	    List<Object> secondColumnValues, List<Object> classesColumnValues,
	    int kneighbors, METRIC metric) throws Exception {
	if (firstColumnValues.size() != secondColumnValues.size()
		|| secondColumnValues.size() != classesColumnValues.size()
		|| kneighbors < 1) {
	    throw new Exception(
		    "K-NN need two value columns with same size, one class column and k-neighbors >= 1.");
	}
	if (!(firstColumnValues.get(0) instanceof Float)) {
	    throw new Exception("First column doesn't contain Float values.");
	}
	if (!(secondColumnValues.get(0) instanceof Float)) {
	    throw new Exception("Second column doesn't contain Float values.");
	}

	return leaveOneOutLogic(firstColumnValues, secondColumnValues,
		classesColumnValues, kneighbors, metric);

    }

    public static Object classification(List<Object> firstColumnValues,
	    List<Object> secondColumnValues, List<Object> classesColumnValues,
	    int kneighbors, METRIC metric, Float firstValue, Float secondValue)
	    throws Exception {
	if (firstColumnValues.size() != secondColumnValues.size()
		|| secondColumnValues.size() != classesColumnValues.size()
		|| kneighbors < 2) {
	    throw new Exception(
		    "K-NN need two value columns with same size, one class column and k-neighbors > 1.");
	}
	if (!(firstColumnValues.get(0) instanceof Float)) {
	    throw new Exception("First column doesn't contain Float values.");
	}
	if (!(secondColumnValues.get(0) instanceof Float)) {
	    throw new Exception("Second column doesn't contain Float values.");
	}

	/*
	 * Tablica w której są przechowywani sąsiedzi danego punktu. Ma ona
	 * wielkość podaną przez użytkownika.
	 */
	Double[] neighborsDistances = new Double[firstColumnValues.size() - 1 >= kneighbors ? kneighbors
		: firstColumnValues.size() - 1];
	Object[] neighborsClasses = new Object[firstColumnValues.size() - 1 >= kneighbors ? kneighbors
		: firstColumnValues.size() - 1];
	Arrays.fill(neighborsClasses, null);
	Arrays.fill(neighborsDistances, null);

	getKNNearestNeighbors(firstColumnValues, secondColumnValues,
		classesColumnValues, kneighbors, metric, neighborsDistances,
		neighborsClasses, -1, firstValue, secondValue);

	/*
	 * W tym momencie mamy dwie tablice zawierające najbliższych sąsiadów.
	 * To tutaj odbywa się podejmowanie decyzji - głosowanie itd. itp.
	 */
	log.info(String.format("Number of neighbors: %d", kneighbors));
	log.info(String.format("Neigbors distances array: %s",
		Arrays.toString(neighborsClasses)));
	log.info(String.format("Neigbors classes array: %s",
		Arrays.toString(neighborsClasses)));

	return vote(neighborsDistances, neighborsClasses);
    }

    public static Map<String, List<Object>> createColumn(
	    String firstColumnName, String secondColumnName,
	    String classesColumnName, List<Object> values, METRIC metric) {

	/*
	 * Tworzenie nazwy nowej kolumny oraz umieszczenie jej razem z
	 * wartościami w HashMapie.
	 */
	String prefix;
	switch (metric) {
	case EUCLIDEAN:
	    prefix = EUCLIDEAN_DISTANCE_PREFIX;
	    break;
	// case MAHALANOBIS:
	// prefix = MAHALANOBIS_DISTANCE_PREFIX;
	// break;
	case MANHATTAN:
	    prefix = MANHATTAN_DISTANCE_PREFIX;
	    break;
	default:
	    prefix = "NO_PREFIX_";
	    break;
	}
	String prefixedName = new StringBuilder(PREFIX_BASE).append(prefix)
		.append(firstColumnName).append("_").append(secondColumnName)
		.append("_").append(classesColumnName).append(CLASSES_SUFFIX)
		.toString();
	Map<String, List<Object>> namedKnndColumn = new LinkedHashMap<String, List<Object>>();
	namedKnndColumn.put(prefixedName, values);

	return namedKnndColumn;
    }

    private static List<Object> leaveOneOutLogic(
	    List<Object> firstColumnValues, List<Object> secondColumnValues,
	    List<Object> classesColumnValues, int kneighbors, METRIC metric) {
	/*
	 * Nowa kolumna reprezentująca klasy obliczone na podstawie sąsiadów
	 * danych elementów.
	 */
	List<Object> classesBasedOnNeighbors = new LinkedList<Object>();

	/*
	 * Tablica w której są przechowywani sąsiedzi danego punktu. Ma ona
	 * wielkość podaną przez użytkownika.
	 */
	Double[] neighborsDistances = new Double[kneighbors];
	Object[] neighborsClasses = new Object[kneighbors];

	/*
	 * Dla każdego elementu obliczamy dystansy wzgledem pozostalych punktow.
	 * Do tablicy zapisujemy jedynie te elementy, ktore sa najblizej.
	 */
	Float mainFirstValue;
	Float mainSecondValue;
	for (int i = 0; i < classesColumnValues.size(); ++i) {
	    mainFirstValue = (Float) firstColumnValues.get(i);
	    mainSecondValue = (Float) secondColumnValues.get(i);

	    Arrays.fill(neighborsClasses, null);
	    Arrays.fill(neighborsDistances, null);

	    getKNNearestNeighbors(firstColumnValues, secondColumnValues,
		    classesColumnValues, kneighbors, metric,
		    neighborsDistances, neighborsClasses, i, mainFirstValue,
		    mainSecondValue);

	    /*
	     * W tym momencie mamy dwie tablice zawierające najbliższych
	     * sąsiadów. To tutaj odbywa się podejmowanie decyzji - głosowanie
	     * itd. itp.
	     */
	    // log.info(String.format("Number of neighbors: %d", kneighbors));
	    // log.info(String.format("Neigbors distances array: %s",
	    // Arrays.toString(neighborsClasses)));
	    // log.info(String.format("Neigbors classes array: %s",
	    // Arrays.toString(neighborsClasses)));

	    // Dodajemy wynik głosowania do listy klas.
	    classesBasedOnNeighbors.add(vote(neighborsDistances,
		    neighborsClasses));
	}

	return classesBasedOnNeighbors;
    }

    private static void getKNNearestNeighbors(List<Object> firstColumnValues,
	    List<Object> secondColumnValues, List<Object> classesColumnValues,
	    int kneighbors, METRIC metric, Double[] neighborsDistances,
	    Object[] neighborsClasses, int currentRow, Float mainFirstValue,
	    Float mainSecondValue) {
	Double maxDistance = null;
	Float secondaryFirstValue;
	Float secondarySecondValue;
	for (int j = 0; j < classesColumnValues.size(); ++j) {
	    // Nie ma potrzeby porównywać tych samych punktów ;)
	    if (j == currentRow)
		continue;

	    secondaryFirstValue = (Float) firstColumnValues.get(j);
	    secondarySecondValue = (Float) secondColumnValues.get(j);

	    // Obliczenie odległości.
	    Double distance;
	    Float diffOne;
	    Float diffTwo;
	    switch (metric) {
	    case EUCLIDEAN:
		diffOne = mainFirstValue - secondaryFirstValue;
		diffTwo = mainSecondValue - secondarySecondValue;
		distance = Math.sqrt((diffOne * diffOne) + (diffTwo * diffTwo));
		break;
	    // case MAHALANOBIS:
	    // throw new NotImplementedException();
	    case MANHATTAN:
		diffOne = mainFirstValue - secondaryFirstValue;
		diffTwo = mainSecondValue - secondarySecondValue;
		distance = Math.sqrt(Math.abs(diffOne) + Math.abs(diffTwo));
		break;
	    default:
		distance = 0.0;
		break;
	    }

	    // Próbujemy wrzucić wartość do tablicy sąsiadów.
	    boolean inserted = false;
	    for (int x = 0; x < kneighbors; ++x) {
		if (neighborsDistances[x] == null) {
		    neighborsDistances[x] = distance;
		    neighborsClasses[x] = classesColumnValues.get(j);
		    inserted = true;
		    break;
		}
	    }

	    // Jeżeli wrzucono i nie istnieje najbardziej oddalony sąsiad...
	    if (inserted && maxDistance == null) {
		maxDistance = distance;
		continue;
	    }

	    /*
	     * Jeżeli wrzucoco i nowy element jest najdalej oddalonym
	     * sąsiadem...
	     */
	    if (inserted && maxDistance != null && maxDistance < distance) {
		maxDistance = distance;
		continue;
	    }

	    /*
	     * Jeżeli nie wrzucono i nowy dystans jest większy niż aktualny
	     * maksymalny...
	     */
	    if (!inserted && distance > maxDistance)
		continue;

	    // Jeżeli nie wrzucono i nowy dystans jest mniejszy niż
	    // maksymalny...
	    if (!inserted && distance < maxDistance) {
		for (int x = 0; x < kneighbors; ++x) {
		    if (neighborsDistances[x] == maxDistance) {
			neighborsDistances[x] = distance;
			neighborsClasses[x] = classesColumnValues.get(j);
		    }
		}

		/*
		 * Wyszukujemy nowa wartość maksymalną po zastepieniu starej
		 * nowym elementem.
		 */
		Double tempMax = null;
		for (int x = 0; x < kneighbors; ++x) {
		    if (tempMax == null) {
			tempMax = neighborsDistances[x];
			continue;
		    } else if (tempMax < neighborsDistances[x]) {
			tempMax = neighborsDistances[x];
		    }
		}
		maxDistance = tempMax;
	    }

	    /*
	     * Pozostała opcja to: nie wrzucono i dystans jest taki sam jak
	     * maksymalny. Jest to problemem jedynie, gdy oba elementy różnią
	     * się klasami. Na razie zostało pominięte.
	     */
	}
    }
}
